summaryrefslogtreecommitdiff
path: root/app/[lng]/admin/edp/components/contract-selector.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]/admin/edp/components/contract-selector.tsx')
-rw-r--r--app/[lng]/admin/edp/components/contract-selector.tsx334
1 files changed, 334 insertions, 0 deletions
diff --git a/app/[lng]/admin/edp/components/contract-selector.tsx b/app/[lng]/admin/edp/components/contract-selector.tsx
new file mode 100644
index 00000000..88986a88
--- /dev/null
+++ b/app/[lng]/admin/edp/components/contract-selector.tsx
@@ -0,0 +1,334 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { Button } from '@/components/ui/button'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
+import { Input } from '@/components/ui/input'
+import { Badge } from '@/components/ui/badge'
+import { Search, Check } from 'lucide-react'
+import {
+ ColumnDef,
+ flexRender,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ useReactTable,
+ SortingState,
+ ColumnFiltersState,
+ VisibilityState,
+ RowSelectionState,
+} from '@tanstack/react-table'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import { getContracts } from '../actions/data-actions'
+import { toast } from 'sonner'
+
+interface Contract {
+ id: number
+ contractNo: string
+ contractName: string
+ status: string
+ projectId: number
+ vendorId: number
+ projectCode: string | null
+ projectName: string | null
+ vendorName: string | null
+ vendorCode: string | null
+}
+
+interface ContractSelectorProps {
+ selectedContract?: Contract
+ onContractSelect: (contract: Contract) => void
+ disabled?: boolean
+ preselectedContractId?: number
+}
+
+export function ContractSelector({ selectedContract, onContractSelect, disabled, preselectedContractId }: ContractSelectorProps) {
+ const [open, setOpen] = useState(false)
+ const [contracts, setContracts] = useState<Contract[]>([])
+ const [loading, setLoading] = useState(false)
+ const [sorting, setSorting] = useState<SortingState>([])
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
+ const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
+ const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
+ const [globalFilter, setGlobalFilter] = useState('')
+
+ const columns: ColumnDef<Contract>[] = [
+ {
+ accessorKey: 'contractNo',
+ header: '계약번호',
+ cell: ({ row }) => (
+ <div className="font-mono text-sm">{row.getValue('contractNo')}</div>
+ ),
+ },
+ {
+ accessorKey: 'contractName',
+ header: '계약명',
+ cell: ({ row }) => (
+ <div className="max-w-[300px] truncate">{row.getValue('contractName')}</div>
+ ),
+ },
+ {
+ accessorKey: 'status',
+ header: '상태',
+ cell: ({ row }) => {
+ const status = row.getValue('status') as string
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'ACTIVE': return 'bg-green-100 text-green-800'
+ case 'TEST': return 'bg-blue-100 text-blue-800'
+ case 'DRAFT': return 'bg-yellow-100 text-yellow-800'
+ case 'PENDING': return 'bg-orange-100 text-orange-800'
+ default: return 'bg-gray-100 text-gray-800'
+ }
+ }
+ return (
+ <Badge className={getStatusColor(status)}>
+ {status}
+ </Badge>
+ )
+ },
+ },
+ {
+ accessorKey: 'projectCode',
+ header: '프로젝트',
+ cell: ({ row }) => {
+ const projectCode = row.getValue('projectCode') as string | null
+ const projectName = row.original.projectName
+ return projectCode ? (
+ <div>
+ <div className="font-mono text-sm">{projectCode}</div>
+ {projectName && (
+ <div className="text-xs text-muted-foreground truncate max-w-[150px]">
+ {projectName}
+ </div>
+ )}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ },
+ {
+ accessorKey: 'vendorName',
+ header: '벤더',
+ cell: ({ row }) => {
+ const vendorName = row.getValue('vendorName') as string | null
+ const vendorCode = row.original.vendorCode
+ return vendorName ? (
+ <div>
+ <div className="font-medium text-sm">{vendorName}</div>
+ {vendorCode && (
+ <div className="text-xs text-muted-foreground font-mono">
+ {vendorCode}
+ </div>
+ )}
+ </div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ },
+ {
+ id: 'actions',
+ header: '선택',
+ cell: ({ row }) => (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => handleContractSelect(row.original)}
+ >
+ <Check className="h-4 w-4" />
+ </Button>
+ ),
+ },
+ ]
+
+ const table = useReactTable({
+ data: contracts,
+ columns,
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ onColumnVisibilityChange: setColumnVisibility,
+ onRowSelectionChange: setRowSelection,
+ onGlobalFilterChange: setGlobalFilter,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ state: {
+ sorting,
+ columnFilters,
+ columnVisibility,
+ rowSelection,
+ globalFilter,
+ },
+ })
+
+ const loadContracts = async () => {
+ setLoading(true)
+ try {
+ const result = await getContracts()
+ if (result.success) {
+ setContracts(result.data)
+
+ // preselectedContractId가 있으면 자동 선택
+ if (preselectedContractId && !selectedContract) {
+ const preselectedContract = result.data.find(c => c.id === preselectedContractId)
+ if (preselectedContract) {
+ onContractSelect(preselectedContract)
+ }
+ }
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ toast.error('계약을 불러오는 중 오류가 발생했습니다.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleContractSelect = (contract: Contract) => {
+ onContractSelect(contract)
+ setOpen(false)
+ }
+
+ useEffect(() => {
+ if (open && contracts.length === 0) {
+ loadContracts()
+ }
+ }, [open])
+
+ // preselectedContractId가 변경되면 계약 목록 다시 로드
+ useEffect(() => {
+ if (preselectedContractId && contracts.length === 0) {
+ loadContracts()
+ }
+ }, [preselectedContractId])
+
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogTrigger asChild>
+ <Button variant="outline" disabled={disabled} className="w-full justify-start">
+ {selectedContract ? (
+ <div className="flex items-center gap-2">
+ <span className="font-mono text-sm">[{selectedContract.contractNo}]</span>
+ <span className="truncate">{selectedContract.contractName}</span>
+ </div>
+ ) : (
+ <span className="text-muted-foreground">계약을 선택하세요</span>
+ )}
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="max-w-5xl max-h-[80vh]">
+ <DialogHeader>
+ <DialogTitle>계약 선택</DialogTitle>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div className="flex items-center space-x-2">
+ <Search className="h-4 w-4" />
+ <Input
+ placeholder="계약번호, 계약명, 프로젝트 코드, 벤더명으로 검색..."
+ value={globalFilter}
+ onChange={(e) => setGlobalFilter(e.target.value)}
+ className="flex-1"
+ />
+ </div>
+
+ {loading ? (
+ <div className="flex justify-center py-8">
+ <div className="text-sm text-muted-foreground">계약을 불러오는 중...</div>
+ </div>
+ ) : (
+ <div className="border rounded-md">
+ <Table>
+ <TableHeader>
+ {table.getHeaderGroups().map((headerGroup) => (
+ <TableRow key={headerGroup.id}>
+ {headerGroup.headers.map((header) => (
+ <TableHead key={header.id}>
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+ </TableHead>
+ ))}
+ </TableRow>
+ ))}
+ </TableHeader>
+ <TableBody>
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+ <TableRow
+ key={row.id}
+ data-state={row.getIsSelected() && "selected"}
+ className="cursor-pointer hover:bg-muted/50"
+ onClick={() => handleContractSelect(row.original)}
+ >
+ {row.getVisibleCells().map((cell) => (
+ <TableCell key={cell.id}>
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+ </TableCell>
+ ))}
+ </TableRow>
+ ))
+ ) : (
+ <TableRow>
+ <TableCell
+ colSpan={columns.length}
+ className="h-24 text-center"
+ >
+ 검색 결과가 없습니다.
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </Table>
+ </div>
+ )}
+
+ <div className="flex items-center justify-between">
+ <div className="text-sm text-muted-foreground">
+ 총 {table.getFilteredRowModel().rows.length}개 계약
+ </div>
+ <div className="flex items-center space-x-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ >
+ 이전
+ </Button>
+ <div className="text-sm">
+ {table.getState().pagination.pageIndex + 1} / {table.getPageCount()}
+ </div>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ >
+ 다음
+ </Button>
+ </div>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+}